Design specification · build-ready · Platform · all six apps
The exhaustive design specification for the platform's shared navigation chrome: the left sidebar (brand row, product switcher, grouped navigation, nested sub-menus, collapse) and the top header bar (App pill, breadcrumbs, global search, notifications, profile, context banners). Every RiverSync web app — Account · Portal · Partners · Pipeline · Admin — wears this exact shell; only the navigation model, the App pill and the search placeholder change per app. This document specifies every region, dimension, state, gating rule and persistence contract of both surfaces — down to the pixel geometry, the role × application switcher matrix and the popover behavior — so an implementer needs no further decisions.
The navigation system is the constant frame around every product surface: it tells the user which app they are in, where inside it they stand, what else their role may reach, and it carries the three global affordances — search, notifications, identity. It is deliberately quiet: square corners, hairline dividers, one accent.
Sentence case everywhere; group labels are styled small-caps (CSS text-transform, never typed caps). No emoji, no third-party icons — every glyph is the proprietary icon system.
The shell is a two-column CSS grid: a sticky full-height sidebar and a main column whose first row is the sticky 40px top header bar. Only the main column scrolls. Collapsing the sidebar animates the grid column 248px → 40px over 220ms; the content reflows, nothing overlaps.
| Token / rule | Value & behavior |
|---|---|
| --sidebar-w | 248px expanded · --sidebar-w-collapsed 40px. Grid column transition 220ms cubic-bezier(.4,0,.2,1). |
| --topbar-h | 40px — the header bar, the brand row, the profile avatar tile and the bell button all share it, so the chrome reads as one 40px rhythm. |
| --nav-cell · --nav-gutter | 40px row height · 10px icon gutter. The row height equals the collapsed width, so a collapsed item is a full-width square. |
| Radii | Shell chrome is square (radius 0) — nav rows, brand row, header, dropdown attachment. Only free-floating menus keep --r-md. |
| Layers | Header z-index:20 · banners 15–16 · popovers 60 · launcher menu 2000 (it escapes the sidebar's overflow:hidden as a fixed layer). |
| Scroll | The sidebar's middle region scrolls independently when the nav outgrows the viewport; brand row and footer stay pinned. |
| Key | Meaning |
|---|---|
| rs-collapsed | Sidebar collapsed state — 1 | 0, applied before first paint (no flash). |
| rs-theme · rs-theme-pref | Resolved theme (light | dark) and the preference (+ system) — toggled from the profile menu. |
| rs-density | compact | comfortable — affects page content rows (38 → 48px); the nav cell stays 40px in both. |
| rs-tenant · rs-role-* · rs-partner-sub | The demo persona the shell resolves at load (defaults customer · Owner, seeded globally). Production replaces this with the session's claims — the keys are prototype plumbing, the resolution order is the spec (FED-15/16/19). |
Three vertically stacked regions: the brand row (which is the product switcher, §5), the scroll region of grouped navigation, and the collapse footer. The sidebar is .rs-side on --surface with a right hairline; it never carries shadows or fills — the active item is the only tinted element.
| Region | Spec |
|---|---|
| Brand row | .rs-side__brand — height --topbar-h, bottom hairline. App-grid glyph (16px, stroke 1.5) in the icon gutter + the RiverSync logotype (18px, SVG asset — never set in type; swaps to the extra-light variant on dark). The whole row is the switcher trigger: hover/open tint --surface-2, open state accents the grid glyph. |
| Group label | .rs-side__label — 10px / 700, letter-spacing .12em, uppercase via CSS, --fg3, padding 16px 16px 6px. Hidden (height 0) when collapsed. |
| Nav item | .rs-nav__item — 40px row, 12.5px / 450 label, 16px icon (stroke 1.6, 8px padded box), padding-left --nav-gutter, square corners. Label truncates with ellipsis; the row is a real <a href> (middle-click and copy-link work). |
| Count pill | .rs-count — right-aligned, 10.5px / 700, 999px radius, --surface-3 fill. Counts are live data, not decoration: open tickets, pending invitations, device totals — each count traces to the surface it fronts and updates with it. |
| Collapse footer | .rs-side__foot — top hairline; one full-width 40px button (chevrons-left icon, right-aligned) toggling rs-collapsed. |
--fg2 text, transparent row. Icon and label only; count pill if the surface carries one.
Row tints --surface-2, text lifts to --fg. 120ms ease. No underline, no movement.
Exactly one leaf: --accent-soft fill, --accent text at 600, and the 3px left accent bar — the accent bar is reserved for this single leaf.
A parent whose descendant is active: text emphasis only (--fg, 600) — never the bar, never the fill.
Verification-gated items (billing, audit, security settings) show a trailing lock glyph and a reason tooltip; the row stays visible — read-locked, not hidden.
The count pill re-renders when its backing datum changes; arriving work also pings the bell (§9.2). No animation on the pill itself.
Nav items may nest at most two levels below a parent. Parents toggle, they never navigate — a parent row carries a chevron, no href. The presentation differs by rail state:
| Rail state | Behavior |
|---|---|
| Expanded — accordion | The branch opens in place: child rows .rs-nav__subitem at 32px height, indented --nav-gutter + 32px, with a 1px guide line spanning the open list. A second level indents one more step (+54px) and the guide line steps with it. Chevron rotates 90°; open branches persist while the page lives; the branch holding the active leaf seeds open. |
| Collapsed — flyout | Clicking the parent icon opens a header-less flyout panel flush to the right of the rail (same row geometry, shadow-lg); a nested branch cascades a second flyout onwards. Outside click and Esc dismiss; collapsing/expanding the rail dismisses any flyout. |
| Active resolution | The active leaf keeps the accent-soft fill + 3px bar (inline or in the flyout); every ancestor reads child-active (text emphasis only). Collapsed, a parent whose descendant is active shows the emphasis on its icon square. |
| Wiring | Vanilla: RS.initNavTree(sideEl) — auto-wired by mountShell. React: nest via items in the nav model. Depth beyond two levels is a modeling error — restructure the information architecture instead. |
The brand row drops a launcher menu: a flush, zero-gap tile grid the exact width of the expanded sidebar, pinned as a fixed layer below the header line (so it escapes the rail's overflow and floats above page content). Each tile is an app the signed-in role may open; clicking one navigates to that app's home without re-authentication — the federated session travels (FED-5).
| Element | Spec |
|---|---|
| Tile | .rs-launchtile — 74px square cell, icon + 11px / 600 label, square corners, hover --surface-2. The current app reads is-current (accent label). |
| Tile icon | .rs-launchtile__ic--badge — a 30×30 tile in the app's canonical App-pill hue (rs-portalbadge--<app>: bg, border and glyph color, both themes), 17px glyph. The badge system is mandatory — a tile never invents its own hue or icon. |
| Navigation | Every entitled tile navigates to the real app home: Account → the tenant's own home (Customer Organization, or RiverSync Overview for the riversync tenant — TEN-3), Portal → Devices, Partners → Overview, Pipeline → Leads, Admin → Console, Field → the mobile app. Website is the only external destination. |
| Dismissal | Outside click, Esc, or choosing a tile. Opening any other popover closes it (one popover at a time, §10.2). |
Tenant-type gating (FED-5) is the outer bound; the signed-in role narrows it — two members of one tenant can hold different tile sets. Apps reachable only through an audited view-as session (riversync → Portal) carry no standing tile. Website always appears (public site, not an entitlement).
| Persona | Role | Tiles |
|---|---|---|
| Customer | Owner · Administrator · Editor · Viewer | Account · Portal |
| Partner — reseller | Administrator | Account · Portal · Partners |
| Service Coordinator · Sales | Partners | |
| Partner — distributor | Administrator | Account · Partners |
| Channel Manager · Sales | Partners | |
| RiverSync | Admin | Account · Pipeline · Admin · Field |
| Support · Accounting | Admin | |
| Sales | Pipeline | |
| Engineer | Field |
Collapsed, the sidebar is a 40px icon rail — every nav row becomes a full-width square (40 × 40) holding only its icon. The state is a deliberate working mode, not a responsive fallback: it persists per user across apps and sessions.
| Element | Collapsed behavior |
|---|---|
| Brand row | The app-grid glyph and logotype hide; the 24px square brand mark takes the row and becomes the switcher trigger. The launcher menu opens at full 248px width regardless. |
| Group labels | Collapse to zero height — groups read as spacing only. |
| Nav items | Icon centered, label and count hidden; the native tooltip (title) carries the label. Active item = a full-square accent-soft fill with the 3px bar at the rail edge. |
| Parents | No inline accordion: the parent icon opens the flyout (§6); nested branches cascade a second flyout. |
| Footer | The collapse button flips to chevrons-right, centered. |
A single 40px sticky row on --surface with a bottom hairline, holding — left to right — the App pill, the breadcrumbs, a flexible spacer, the search field, the notification bell and the profile button flush to the viewport edge. Nothing in the header scrolls away; it is the constant compass.
| Element | Spec |
|---|---|
| App pill | rs-portalbadge--sm rs-portalbadge--<app> — the canonical small App pill: icon tile + uppercase label, one domain hue per app (Account neutral · Portal cyan · Partners teal · Pipeline gold · Admin purple · Field red). Mandatory on every app page; never restyled locally. |
| Breadcrumbs | .rs-top__crumbs — 13px, chevron separators. First crumb = the organization name (links to the app home), then the trail; the last crumb is the current page in --fg, not a link. The trail truncates from the middle on narrow widths — first and last always survive. |
| Search | §9.1. |
| Bell · profile | §9.2 · §10 — a full-height cluster; the bell is a flush 40px square, the avatar tile bleeds to the viewport edge (no right margin). |
| Element | Spec |
|---|---|
| Field | .rs-top__search — 28px tall, 340px wide (flex:0 1 340px, min 170px), --surface-2 fill, hairline border, --r-md. 15px search glyph left, ⌘K keycap right (mono, hidden on touch). |
| Placeholder | Per app, naming its three primary nouns: "Search users, locations, roles…" (Account) · "Search devices, sites, alerts…" (Portal) · "Search tenants, users, invoices…" (Admin) · "Search customers, devices, tickets…" (operations surfaces). |
| Shortcut | ⌘K / Ctrl K focuses the field from anywhere; Esc blurs and clears. |
| Scope | Search is app-scoped — it never reaches across apps. Results and their ranking belong to each app's own surface spec, not the chrome. |
| Element | Spec |
|---|---|
| Bell button | Flush square, 40 × 40 (full header height), square corners. Unread badge .rs-notif-badge: ≥15px round chip, --danger fill, white 9.5px / 700 tabular count, top-right of the glyph. Hidden at zero. |
| Panel | .rs-notif — 360px dropdown attached flush below the button (square top corners, --shadow-lg). Head: "Notifications" + a "N new" count chip + Mark all read. List scrolls at 392px max. Foot: "View all notifications". |
| Item | 34px round tone tile (success · warning · danger · info · accent — icon per event type) · 12.5px / 600 title · 12px body · 11px relative time · an 8px accent unread dot. Unread rows sit on --accent-soft; clicking a row marks it read and deep-links its subject. |
| Copy | Notification bodies follow the portal voice — terse, operational, numbers carry the meaning: "MDC-6610 return air at 41.4°C — 6.4°C above setpoint." |
Arriving work follows one notify pattern, platform-wide — the same contract the chat and ticket systems use:
| Element | Spec |
|---|---|
| Button | .rs-profilebtn — full header height, flush to the viewport's right edge. A 40px square avatar tile (initials, calm neutral fill), then a two-line meta (name 12.5px / 600 · role 11px --fg3), then a chevron. A per-tenant hue class tints the tile subtly (customer · partner · riversync) so the signed-in context is legible at a glance. |
| Menu | 240px dropdown, flush square attach. Items in order: My account · Preferences · Dark mode / Light mode (theme toggle, relabels live) · one context link per app (e.g. Account portal from Portal, Admin console from operations, Organization from a customer Account page) · separator · Help & support · Sign out (danger tone). |
| Workspace switch | Where a user's email holds accounts in several tenants, the sign-in workspace picker (SPEC-PWF-AUT) is the switch point — the profile menu links back to it via Sign out. One session = one tenant persona; there is no in-session tenant blend. |
| Banner | Spec |
|---|---|
| Email verification | .rs-verifybar — sticky at top: --topbar-h (z 15), --warning-soft fill + warning hairline. States what unlocks and how: "Verify your email to activate your account" + Resend email / Enter verification code. While unverified, gated nav items carry the lock state (§4.1). |
| View-as (audited) | .rs-viewasbar — sticky (z 16), amber diagonal-stripe fill so it can never be mistaken for content. Eye glyph + "Viewing {tenant} as RiverSync …. Every action is recorded." + an Exit button returning to the operator's console. Present on every page of the impersonated session. |
| Stacking | Banners stack in DOM order below the header; page content offsets accordingly. Never more than two — verification and view-as are the only chrome banners. |
The switcher launcher, the notification panel, the profile menu and any collapsed-rail flyout share one behavioral contract:
| Rule | Behavior |
|---|---|
| One at a time | Opening any popover closes every other. State lives on the trigger's .rs-pop.is-open. |
| Dismissal | Outside click · Esc · choosing an item. Focus returns to the trigger. |
| Motion | Fade + 4px rise, 130ms --ease. Header dropdowns attach flush (square corners, no gap); free menus float at +5px with --r-md. prefers-reduced-motion collapses all of it to opacity. |
| Focus ring | All triggers show the 3px soft ring (--ring) on :focus-visible only. |
The shell resolves the signed-in persona once at load, then renders three gated layers: which app tiles the switcher offers (§5.1), which nav model the sidebar mounts, and which rows inside it are locked. The chrome never invents access — it renders the Federation model's answer.
| App | Navigation model & gating |
|---|---|
| Account | Per tenant type: the customer/partner org model (Organization · Users · Departments · Site locations · Roles & access · Billing · Invoices · Audit) vs the riversync model (Overview · Users · Roles · Permissions — no organization management, TEN-3). Verification-locked rows per §10.2. |
| Portal | Customer monitoring nav (Monitoring: Devices; Support: Schedule · Tickets). Alarms and maintenance are not standalone menus — both are read on the device detail (its Alarms / Maintenance tabs, SPEC-UIX-DEV). Every customer role sees every surface; actions gate inside. |
| Partners | Subtype-specialized: reseller nav (Deals · covered Devices · Renewals …) vs distributor nav (Funnel · Resellers · Distribution agreements …). One subtype per partner organization, RiverSync-set (PRT-9). |
| Pipeline | RiverSync-only; entitled to the Sales and Admin roles (master AUTH-2). Other riversync roles get the lock panel naming the entitled roles — the page never half-renders. |
| Admin | RiverSync-only console; every riversync role signs in, and the sidebar re-gates per role (support sees service surfaces, accounting sees plans & invoices, admin sees all). |
| Field | Out of shell scope — its own mobile chrome (SPEC-APP-FLD); reached from the switcher as a tile for engineer/admin. |
| Element | Class / component | DS basis |
|---|---|---|
| Shell frame | .rs-app · .rs-side · .rs-main · .rs-top | tokens/app-ui.css §4 — grid, sticky chrome, geometry tokens. |
| Vanilla shell | RS.mountShell(opts) | rs-shell.js — Account · Partners · Pipeline · Admin pages. |
| React shell | AppShell (portal · account · ops · admin) | DS bundle — Portal; same classes, same tokens, mirrored nav model. |
| Nav rows | .rs-nav__item · .rs-nav__subitem · .rs-count | 40px cells / 32px sub-rows, accent-soft active + 3px bar. |
| Nested nav | .rs-nav__branch · .rs-nav__sub · .rs-navfly__* | RS.initNavTree / items — accordion + flyout. |
| Product switcher | .rs-brandbtn · .rs-launch · .rs-launchtile | tokens/shell-chrome.css — fixed launcher, badge tiles. |
| App pill | rs-portalbadge--sm rs-portalbadge--<app> | Canonical App pill, six domain hues, both themes. Mandatory. |
| Search | .rs-top__search + kbd | App-UI kit; JetBrains Mono keycap. |
| Notifications | .rs-notif · .rs-notif-badge | App-UI kit — tone tiles use the semantic soft pairs. |
| Profile | .rs-profilebtn · .rs-av · .rs-menu | App-UI kit; per-tenant hue modifier. |
| Banners | .rs-verifybar · .rs-viewasbar | Warning-soft band · striped amber view-as band. |
| Icons | RS.icon('…') / <Icon name="…"/> | Proprietary icon system (the G table) — never a third-party set. |
Icons in use: app-grid · building · activity · hub · trend-up · shield-check · wrench · globe · search · bell · chevron-down/-right · chevrons-left/-right · user · settings · sun · moon · help-circle · log-out · eye · lock plus each app's nav glyphs. Any new glyph joins the DS G table first.
| Context | Behavior |
|---|---|
| Narrow desktop | The search field flexes 340 → 170px; breadcrumbs truncate from the middle (first and last crumbs survive); the profile meta drops its second line before the name. |
| Tablet | The rail defaults collapsed; the notification panel caps at 100vw − 28px. |
| Mobile | The desktop shell is not the mobile answer — mobile-first surfaces (Field) carry their own chrome. Desktop apps remain usable via the collapsed rail; no hamburger pattern is introduced. |
| Keyboard | Full traversal: rail items and header controls are tabbable in visual order; ⌘K focuses search; Esc closes any popover; Enter activates. Parents toggle with Enter/Space. |
| Assistive | The sidebar is <aside> + <nav> landmarks; the switcher button carries aria-label="Switch product"; count pills are announced with their surface ("Tickets, 3"); active state is aria-current="page". Color never carries meaning alone — active pairs fill + bar + weight. |
| Motion | All chrome motion (collapse 220ms, popovers 130ms) honors prefers-reduced-motion. |
The prototype is the source of truth for look & behavior; these are the deltas a production build must close.
| Area | As-built | Production target |
|---|---|---|
| Persona resolution | localStorage keys (rs-tenant · rs-role-*), seeded Customer · Owner | Session claims from the identity provider resolve tenant, role and entitlements (FED-15/16/19); the demo keys disappear. |
| Switcher targets | sibling prototype pages (../<app>/…) | The real subdomains (account · portal · partner · pipeline · admin · field .riversync.com) under the federated session — still no re-auth (FED-5). |
| Notifications | static demo set per app | A real notification service (the ACC-6 ⚠ gap) feeding the §9.2 pattern; unread state server-held. |
| Search | visual affordance only | App-scoped search with a keyboard-navigable result list; ranking per app spec. |
| Tweaks balloon | floating persona switcher on every page | Removed. Prototype scaffolding only — marked editor chrome, excluded from captures, mandated out of the hand-off (see the project rules). No SPEC/FED requirement mandates it. |
| This spec | Traces to |
|---|---|
| Product switcher (§5) | FED-5 (membership-gated, SSO, no re-auth) · FED-18 (role × application matrix) · AUTH-2 (entitlement) · TEN-3 (riversync Account home). |
| Sidebar nav models (§4, §11) | Each app's requirement set (SPEC-APP-ACC / -PTL / -PAR / -PIP / -ADM) · FED-6 · FED-9 (roles & permission scopes) · PRT-9 (partner subtype). |
| Verification banner & locks (§10.2) | The sign-in & verification process (SPEC-PWF-AUT); gated surfaces per the Account PRD. |
| View-as banner (§10.2) | The audited view-as session (Admin PRD; both audit trails, DM-20). |
| Notify pattern (§9.2) | The chat/ticket notify contract (SPEC-UIX-TKT) · notifications service gap ACC-6 ⚠. |
| Cross-links | Surface specs inside the frame: SPEC-UIX-DEV · SPEC-UIX-TKT · SPEC-UIX-SCH · SPEC-UIX-COM. |
| Version | Date | Changes |
|---|---|---|
| 1.1 | 2 Jul 2026 | Portal nav model trimmed (§11). The Portal gating row now reads Monitoring: Devices · Support: Schedule · Tickets — the standalone Alerts & alarms and Maintenance menus are removed; both are read on the device detail's Alarms / Maintenance tabs (SPEC-UIX-DEV v1.5, SPEC-APP-PTL v0.24). Chrome contract unchanged. |
| 1.0 | 2 Jul 2026 | First issue — the platform layout navigation system specified as built: the shell frame & geometry tokens (248/40/40px, persistence contract), the left sidebar (brand row, group labels, nav items & states, count-pill data contract, collapse footer), the product switcher (launcher geometry, canonical badge tiles, real-navigation contract, the FED-18 role × application matrix), nested navigation (two-level accordion + collapsed flyouts), the collapsed rail, the top header bar (App pill, breadcrumbs, ⌘K search, notification bell + panel + the five-step notify pattern, full-height profile button + menu), the context banners (verification, audited view-as), the popover contract, tenant & role gating per app, full design-system mapping, responsive & accessibility, as-built vs production (incl. the Tweaks balloon exclusion), and traceability. A presentation view over the Federation model — no new entities or rules. Added to the User interface experience family (SPEC-UIX-NAV) and registered in docs-nav + the design index. |